package core

import (
	"fmt"
	"math"
	"net/http"
	"sort"
	"strconv"
	"strings"

	"codeberg.org/vnpower/pixivfe/v2/audit"
	"codeberg.org/vnpower/pixivfe/v2/i18n"
	"codeberg.org/vnpower/pixivfe/v2/server/session"
	"github.com/goccy/go-json"
	"go.uber.org/zap"
)

// UserWorkCategory represents a unified structure for handling different work categories.
type UserWorkCategory struct {
	Value        string         // Category identifier (e.g., "illustrations", "manga")
	PageLimit    int            // Maximum number of pages for pagination
	FrequentTags []FrequentTag  // Frequently used tags within the category
	IllustWorks  []ArtworkBrief // Populated if the work type requires ArtworkBrief
	NovelWorks   []NovelBrief   // Populated if the work type requires NovelBrief
	WorkIDs      string         // Concatenated string of work IDs
	WorkCount    int            // Number of works for the category
	MangaSeries  []MangaSeries  // Populated for the "manga" category
	NovelSeries  []NovelSeries  // Populated for the "novels" category
}

var (
	CategoryAny          = &UserWorkCategory{Value: ""}
	CategoryAnyAlt       = &UserWorkCategory{Value: "artworks"}
	CategoryIllustration = &UserWorkCategory{Value: "illustrations"}
	CategoryManga        = &UserWorkCategory{Value: "manga"}
	CategoryBookmarks    = &UserWorkCategory{Value: "bookmarks"}
	CategoryNovels       = &UserWorkCategory{Value: "novels"}
)

// NewUserWorkCategory creates a new UserWorkCategory with the given value
func NewUserWorkCategory(value string) UserWorkCategory {
	return UserWorkCategory{Value: value, IllustWorks: nil, WorkCount: 0}
}

func (s UserWorkCategory) Validate() error {
	validValues := []string{
		CategoryAny.Value,
		CategoryAnyAlt.Value,
		CategoryIllustration.Value,
		CategoryManga.Value,
		CategoryBookmarks.Value,
		CategoryNovels.Value,
	}

	for _, val := range validValues {
		if s.Value == val {
			return nil
		}
	}
	return i18n.Errorf(`Invalid work category: %#v. Only "%s", "%s", "%s", "%s", "%s" and "%s" are available`,
		s.Value, CategoryAny.Value, CategoryAnyAlt.Value, CategoryIllustration.Value, CategoryManga.Value, CategoryBookmarks.Value, CategoryNovels.Value)
}

func (c *UserWorkCategory) SetPageLimit(limit int) {
	c.PageLimit = limit
}

type FrequentTag struct {
	Name           string `json:"tag"`
	TranslatedName string `json:"tag_translation"`
}

// User represents a user.
//
// Note that the Artworks slice includes both illustrations and manga, but not novels,
// and is also used to hold the []ArtworkBrief when viewing a user's bookmarks (TODO to separate?)
type User struct {
	ID              string          `json:"userId"`
	Name            string          `json:"name"`
	Avatar          string          `json:"imageBig"`
	Following       int             `json:"following"`
	MyPixiv         int             `json:"mypixivCount"`
	CommentHTML     HTML            `json:"commentHtml"`
	Comment         string          `json:"comment"` // /ajax/discovery doesn't expose commentHtml (pixiv moment)
	Webpage         string          `json:"webpage"`
	SocialRaw       json.RawMessage `json:"social"`
	Background      map[string]any  `json:"background"`
	Social          map[string]map[string]string
	BackgroundImage string

	// Categories holds all category-related data,
	// including Artworks, Illustrations, Manga, Novels, NovelSeries, MangaSeries
	Categories map[string]*UserWorkCategory

	IsFollowed bool `json:"isFollowed"` // Denotes whether the logged in user is currently following the given user

	// Following fields are used on the user discovery page only
	Artworks []ArtworkBrief
	Novels   []NovelBrief
}

// GetUserProfile retrieves the user profile, including counts, artworks/bookmarks, and social data.
func GetUserProfile(auditor *audit.Auditor, r *http.Request, id string, currentCategory *UserWorkCategory, page int, getTags bool) (User, error) {
	var user User
	auditor.Logger.Debug("GetUserProfile called",
		zap.String("userID", id),
		zap.String("currentCategory", currentCategory.Value),
		zap.Int("page", page),
		zap.Bool("getTags", getTags),
	)

	// Fetch user information
	userInfoURL := GetUserInformationURL(id)
	auditor.Logger.Debug("Fetching user information", zap.String("URL", userInfoURL))
	token := session.GetUserToken(r)
	resp, err := API_GET_UnwrapJson(r.Context(), auditor, userInfoURL, token, r.Header)
	if err != nil {
		auditor.SugaredLogger.Error("Failed to fetch user information", zap.Error(err))
		return user, err
	}

	resp, err = session.ProxyImageUrl(r, resp)
	if err != nil {
		auditor.SugaredLogger.Error("Failed to proxy image URL", zap.Error(err))
		return user, err
	}

	if err := json.Unmarshal([]byte(resp), &user); err != nil {
		auditor.SugaredLogger.Error("Failed to unmarshal user information", zap.Error(err))
		return user, err
	}

	// Initialize the user categories
	user.Categories = map[string]*UserWorkCategory{
		"illustrations": {Value: "illustrations"},
		"manga":         {Value: "manga"},
		"novels":        {Value: "novels"},
		"bookmarks":     {Value: "bookmarks"},
	}

	// Get populated works or bookmarks based on the currentCategory
	auditor.Logger.Debug("Getting populated works")
	if err := getPopulatedWorks(auditor, r, &user, id, currentCategory, page, getTags); err != nil {
		auditor.SugaredLogger.Error("Failed to get populated works", zap.Error(err))
		return user, err
	}

	// Parse social data
	auditor.Logger.Debug("Parsing social data")
	if err := user.parseSocial(); err != nil {
		auditor.SugaredLogger.Error("Failed to parse social data", zap.Error(err))
		return User{}, err
	}
	auditor.Logger.Debug("Social data parsed", zap.Any("social", user.Social))

	// Set background image if available
	if user.Background != nil {
		if url, ok := user.Background["url"].(string); ok {
			user.BackgroundImage = url
			auditor.Logger.Debug("Background image set", zap.String("BackgroundImage", url))
		} else {
			auditor.SugaredLogger.Warn("Background URL not found or invalid")
		}
	}

	return user, nil
}

// getPopulatedWorks fetches then populates information for a user's works or bookmarks, based on the currentCategory.
//
// It also handles a user's frequently used tags and series data.
func getPopulatedWorks(auditor *audit.Auditor, r *http.Request, user *User, id string, currentCategory *UserWorkCategory, page int, getTags bool) error {
	auditor.Logger.Debug("getPopulatedWorks called",
		zap.String("userID", id),
		zap.String("currentCategory", currentCategory.Value),
		zap.Int("page", page),
		zap.Bool("getTags", getTags),
	)

	// Fetch work IDs and series data
	auditor.Logger.Debug("Fetching work IDs and series data for works")
	categoriesData, err := fetchWorkIDsAndSeriesData(auditor, r, id, currentCategory, page)
	if err != nil {
		auditor.SugaredLogger.Error("Failed to fetch work IDs and series data", zap.Error(err))
		return err
	}

	// Update user.Categories with the fetched IDs and series data
	for name, catData := range categoriesData {
		user.Categories[name] = catData
	}

	// Fetch and populate works for each category
	for _, cat := range user.Categories {
		if cat.WorkIDs == "" && cat.Value != "bookmarks" {
			continue
		}

		switch cat.Value {
		case "illustrations", "manga":
			auditor.Logger.Debug("Fetching and populating ArtworkBriefs", zap.String("category", cat.Value))
			artworks, err := fetchPopulatedArtworkBriefs(auditor, r, id, cat.WorkIDs, fetchArtworkIDs)
			if err != nil {
				auditor.SugaredLogger.Error("Failed to fetch populated ArtworkBriefs", zap.Error(err))
				return err
			}
			cat.IllustWorks = artworks

		case "novels":
			auditor.Logger.Debug("Fetching and populating NovelBriefs", zap.String("category", cat.Value))
			novels, err := fetchPopulatedNovelBriefs(auditor, r, id, cat.WorkIDs, fetchNovelIDs)
			if err != nil {
				auditor.SugaredLogger.Error("Failed to fetch populated NovelBriefs", zap.Error(err))
				return err
			}
			cat.NovelWorks = novels

		case "bookmarks":
			auditor.Logger.Debug("Fetching and populating Bookmarks", zap.String("category", cat.Value))
			bookmarks, count, err := fetchBookmarks(auditor, r, id, "show", page)
			if err != nil {
				auditor.SugaredLogger.Error("Failed to fetch bookmarks", zap.Error(err))
				return err
			}
			cat.IllustWorks = bookmarks
			cat.WorkCount = count
			worksPerPage := 48.0
			cat.PageLimit = int(math.Ceil(float64(count) / worksPerPage))
		}

		// Fetch frequent tags if requested
		if getTags && cat.Value != "bookmarks" {
			auditor.Logger.Debug("Fetching frequent tags for category", zap.String("category", cat.Value))
			tags, err := fetchFrequentTags(auditor, r, cat.WorkIDs, cat)
			if err != nil {
				auditor.SugaredLogger.Error("Failed to fetch frequent tags", zap.String("category", cat.Value), zap.Error(err))
				return err
			}
			cat.FrequentTags = tags
		}
	}

	// Set PageLimit for the current category
	if currentCategory != nil {
		if cat, ok := user.Categories[currentCategory.Value]; ok {
			currentCategory.PageLimit = cat.PageLimit
		}
	}

	return nil
}

// fetchWorkIDsAndSeriesData retrieves artwork IDs and series information for a user.
//
// It populates UserWorkCategory structs for each category.
func fetchWorkIDsAndSeriesData(auditor *audit.Auditor, r *http.Request, id string, currentCategory *UserWorkCategory, page int) (map[string]*UserWorkCategory, error) {
	auditor.Logger.Debug("fetchWorkIDsAndSeriesData called",
		zap.String("userID", id),
		zap.String("currentCategory", currentCategory.Value),
		zap.Int("page", page),
	)

	URL := GetUserWorksURL(id)
	auditor.Logger.Debug("Fetching user works", zap.String("URL", URL))

	resp, err := API_GET_UnwrapJson(r.Context(), auditor, URL, "", r.Header)
	if err != nil {
		auditor.SugaredLogger.Error("API_GET_UnwrapJson failed", zap.Error(err))
		return nil, err
	}

	auditor.Logger.Debug("User works fetched", zap.String("response", resp))

	resp, err = session.ProxyImageUrl(r, resp)
	if err != nil {
		auditor.SugaredLogger.Error("Failed to proxy image URL", zap.Error(err))
		return nil, err
	}

	var body struct {
		Illusts     *json.RawMessage `json:"illusts"`
		Manga       *json.RawMessage `json:"manga"`
		MangaSeries *json.RawMessage `json:"mangaSeries"`
		Novels      *json.RawMessage `json:"novels"`
		NovelSeries *json.RawMessage `json:"novelSeries"`
	}

	err = json.Unmarshal([]byte(resp), &body)
	if err != nil {
		auditor.SugaredLogger.Error("Failed to unmarshal user works response", zap.Error(err))
		return nil, fmt.Errorf("failed to unmarshal response body: %w", err)
	}
	auditor.Logger.Debug("User works response unmarshalled")

	categories := make(map[string]*UserWorkCategory)

	// Process illustrations
	illustCat := &UserWorkCategory{Value: "illustrations"}
	illustIDs, count, err := extractIDs(body.Illusts, "illusts")
	if err != nil {
		auditor.SugaredLogger.Error("Failed to extract illustration IDs", zap.Error(err))
		return nil, err
	}
	illustCat.WorkCount = count
	illustCat.WorkIDs = buildIDString(auditor, illustIDs, page, currentCategory, illustCat)
	categories["illustrations"] = illustCat

	// Process manga
	mangaCat := &UserWorkCategory{Value: "manga"}
	mangaIDs, count, err := extractIDs(body.Manga, "manga")
	if err != nil {
		auditor.SugaredLogger.Error("Failed to extract manga IDs", zap.Error(err))
		return nil, err
	}
	mangaCat.WorkCount = count
	mangaCat.WorkIDs = buildIDString(auditor, mangaIDs, page, currentCategory, mangaCat)
	// Handle MangaSeries
	if body.MangaSeries != nil && len(*body.MangaSeries) > 0 {
		err := json.Unmarshal(*body.MangaSeries, &mangaCat.MangaSeries)
		if err != nil {
			auditor.SugaredLogger.Error("Failed to unmarshal MangaSeries", zap.Error(err))
		}
	}
	categories["manga"] = mangaCat

	// Process novels
	novelCat := &UserWorkCategory{Value: "novels"}
	novelIDs, count, err := extractIDs(body.Novels, "novels")
	if err != nil {
		auditor.SugaredLogger.Error("Failed to extract novel IDs", zap.Error(err))
		return nil, err
	}
	novelCat.WorkCount = count
	novelCat.WorkIDs = buildIDString(auditor, novelIDs, page, currentCategory, novelCat)
	// Handle NovelSeries
	if body.NovelSeries != nil && len(*body.NovelSeries) > 0 {
		err := json.Unmarshal(*body.NovelSeries, &novelCat.NovelSeries)
		if err != nil {
			auditor.SugaredLogger.Error("Failed to unmarshal NovelSeries", zap.Error(err))
		}
	}
	categories["novels"] = novelCat

	// Create an empty bookmarks category
	//
	// We don't build an ID string here as fetchBookmarks populates IllustWorks without the need for a WorkIDs string
	// (which is also why we can't call fetchFrequentTags for the "bookmarks" category, though extracting the IDs from
	// ArtworkBrief would be possible)
	categories["bookmarks"] = &UserWorkCategory{Value: "bookmarks"}

	return categories, nil
}

// buildIDString builds the ID string for API requests and sets the PageLimit.
func buildIDString(auditor *audit.Auditor, ids []int, page int, currentCategory, cat *UserWorkCategory) string {
	sort.Sort(sort.Reverse(sort.IntSlice(ids)))
	worksPerPage := 30.0
	totalItems := len(ids)

	// We only use the actual page number for the current category being viewed
	// and default the page to 1 for other categories.
	//
	// This is so that we don't attempt to paginate categories that don't have enough
	// items, which would raise a spurious error from computeSliceBounds regarding an
	// invalid page number.
	//
	// NOTE: A different approach will be needed if we require pageLimits for inactive categories.
	effectivePage := page
	if currentCategory.Value != cat.Value {
		effectivePage = 1
	}

	start, end, pageLimit, err := computeSliceBounds(auditor, effectivePage, worksPerPage, totalItems)
	if err != nil {
		auditor.SugaredLogger.Error("Error computing slice bounds", zap.Error(err))
		return ""
	}

	if currentCategory.Value == cat.Value {
		cat.SetPageLimit(pageLimit)
	}

	idsToUse := ids[start:end]
	var idsBuilder strings.Builder
	for _, k := range idsToUse {
		idsBuilder.WriteString(fmt.Sprintf("&ids[]=%d", k))
	}
	return idsBuilder.String()
}

// extractIDs is a helper function to extract IDs and count from a raw JSON message
func extractIDs(rawMessage *json.RawMessage, categoryName string) ([]int, int, error) {
	var dataMap IntStringMap
	if rawMessage != nil && len(*rawMessage) > 0 {
		if err := json.Unmarshal(*rawMessage, &dataMap); err != nil {
			fmt.Printf("Error unmarshalling %s: %v\n", categoryName, err)
			dataMap = make(IntStringMap)
		}
	} else {
		dataMap = make(IntStringMap)
	}

	ids := make([]int, 0, len(dataMap))
	for k := range dataMap {
		ids = append(ids, k)
	}
	return ids, len(dataMap), nil
}

// fetchPopulatedArtworkBriefs fetches and processes ArtworkBrief items.
func fetchPopulatedArtworkBriefs(
	auditor *audit.Auditor,
	r *http.Request,
	id, ids string,
	fetchFunc func(*audit.Auditor, *http.Request, string, string) ([]ArtworkBrief, error),
) ([]ArtworkBrief, error) {
	items, err := fetchFunc(auditor, r, id, ids)
	if err != nil {
		return nil, err
	}

	// Sort the items based on ID in descending order
	sort.Slice(items, func(i, j int) bool {
		return numberGreaterThan(items[i].ID, items[j].ID)
	})

	// Populate thumbnails for each artwork
	for idx := range items {
		artwork := &items[idx]
		if err := artwork.PopulateThumbnails(auditor); err != nil {
			auditor.SugaredLogger.Errorf("Failed to populate thumbnails for artwork ID %s: %v", artwork.ID, err)
			return nil, fmt.Errorf("failed to populate thumbnails for artwork ID %s: %w", artwork.ID, err)
		}
	}

	return items, nil
}

// fetchPopulatedNovelBriefs fetches and processes NovelBrief items.
func fetchPopulatedNovelBriefs(
	auditor *audit.Auditor,
	r *http.Request,
	id, ids string,
	fetchFunc func(*audit.Auditor, *http.Request, string, string) ([]NovelBrief, error),
) ([]NovelBrief, error) {
	items, err := fetchFunc(auditor, r, id, ids)
	if err != nil {
		return nil, err
	}

	// Sort the items based on ID in descending order
	sort.Slice(items, func(i, j int) bool {
		return numberGreaterThan(items[i].ID, items[j].ID)
	})

	return items, nil
}

// fetchFrequentTags fetches a user's frequently used tags, based on category.
func fetchFrequentTags(auditor *audit.Auditor, r *http.Request, ids string, workCategory *UserWorkCategory) ([]FrequentTag, error) {
	var tags []FrequentTag
	var URL string

	switch workCategory.Value {
	case "illustrations", "manga":
		URL = GetUserFrequentArtworkTagsURL(ids)
	case "novels":
		URL = GetUserFrequentNovelTagsURL(ids)
	default:
		return tags, fmt.Errorf("unsupported category: %s", workCategory.Value)
	}

	// Return early if there are no IDs
	//
	// NOTE: theoretically, this check should never evaluate as true since fetchUserWorks doesn't call
	// getUserFrequentTags when len(tagsIDs) == 0, instead returning an empty []FrequentTag
	if ids == "" {
		return tags, nil
	}

	response, err := API_GET_UnwrapJson(r.Context(), auditor, URL, "", r.Header)
	if err != nil {
		return nil, err
	}

	err = json.Unmarshal([]byte(response), &tags)
	if err != nil {
		return nil, err
	}

	return tags, nil
}

// Work is a generic type constraint.
type Work interface{}

// fetchWorkIDs is a generic helper function to fetch work IDs.
func fetchWorkIDs[T Work](auditor *audit.Auditor, r *http.Request, url string) ([]T, error) {
	resp, err := API_GET_UnwrapJson(r.Context(), auditor, url, "", r.Header)
	if err != nil {
		return nil, err
	}

	resp, err = session.ProxyImageUrl(r, resp)
	if err != nil {
		return nil, err
	}

	// Define a generic body structure.
	var body struct {
		Works map[int]json.RawMessage `json:"works"`
	}

	// Unmarshal the response into the body.
	if err := json.Unmarshal([]byte(resp), &body); err != nil {
		return nil, err
	}

	// Initialize the slice to hold the works.
	var works []T

	// Iterate over each work and unmarshal into the specific type.
	for _, v := range body.Works {
		var work T
		if err := json.Unmarshal(v, &work); err != nil {
			return nil, err
		}
		works = append(works, work)
	}

	return works, nil
}

// fetchArtworkIDs fetches the list of artwork IDs for a user (without other data).
func fetchArtworkIDs(auditor *audit.Auditor, r *http.Request, id, ids string) ([]ArtworkBrief, error) {
	URL := GetUserFullArtworkURL(id, ids)

	works, err := fetchWorkIDs[ArtworkBrief](auditor, r, URL)
	if err != nil {
		return nil, err
	}

	return works, nil
}

// fetchNovelIDs fetches the list of novel IDs for a user (without other data).
func fetchNovelIDs(auditor *audit.Auditor, r *http.Request, id, ids string) ([]NovelBrief, error) {
	URL := GetUserFullNovelURL(id, ids)

	works, err := fetchWorkIDs[NovelBrief](auditor, r, URL)
	if err != nil {
		return nil, err
	}

	return works, nil
}

// fetchBookmarks fetches the list of bookmarks for a user (with other data).
//
// This function cannot be neatly refactored to use getWorkIDs due to having
// a different API response structure.
func fetchBookmarks(auditor *audit.Auditor, r *http.Request, id, mode string, page int) ([]ArtworkBrief, int, error) {
	page--

	URL := GetUserBookmarksURL(id, mode, page)

	resp, err := API_GET_UnwrapJson(r.Context(), auditor, URL, "", r.Header)
	if err != nil {
		return nil, -1, err
	}

	resp, err = session.ProxyImageUrl(r, resp)
	if err != nil {
		return nil, -1, err
	}

	var body struct {
		Artworks []json.RawMessage `json:"works"`
		Total    int               `json:"total"`
	}

	err = json.Unmarshal([]byte(resp), &body)
	if err != nil {
		return nil, -1, err
	}

	artworks := make([]ArtworkBrief, len(body.Artworks))

	for index, value := range body.Artworks {
		var artwork ArtworkBrief

		err = json.Unmarshal([]byte(value), &artwork)
		if err != nil {
			artworks[index] = ArtworkBrief{
				ID:        "#",
				Title:     "Deleted or Private",
				Thumbnail: "https://s.pximg.net/common/images/limit_unknown_360.png",
			}
			continue
		}

		// Populate thumbnails
		if err := artwork.PopulateThumbnails(auditor); err != nil {
			auditor.SugaredLogger.Errorf("Failed to populate thumbnails for artwork ID %s: %v", id, err)
			return nil, -1, fmt.Errorf("failed to populate thumbnails for artwork ID %s: %w", id, err)
		}
		artworks[index] = artwork
	}

	return artworks, body.Total, nil
}

// computeSliceBounds is a utility function to compute slice bounds safely
func computeSliceBounds(auditor *audit.Auditor, page int, worksPerPage float64, totalItems int) (start, end, pageLimit int, err error) {
	auditor.Logger.Debug("Entered computeSliceBounds",
		zap.Int("page", page),
		zap.Float64("worksPerPage", worksPerPage),
		zap.Int("totalItems", totalItems),
	)

	if totalItems == 0 {
		auditor.Logger.Debug("totalItems is 0, returning 0, 0, nil")
		return 0, 0, 0, nil
	}

	pageLimit = int(math.Ceil(float64(totalItems) / worksPerPage))
	auditor.Logger.Debug("Calculated pageLimit",
		zap.Int("pageLimit", pageLimit),
	)

	if page < 1 || page > pageLimit {
		auditor.Logger.Debug("Invalid page number",
			zap.Int("page", page),
			zap.Int("pageLimit", pageLimit),
		)
		return 0, 0, 0, fmt.Errorf("invalid page number")
	}

	start = (page - 1) * int(worksPerPage)
	end = min(start+int(worksPerPage), totalItems)

	auditor.Logger.Debug("Calculated slice bounds",
		zap.Int("start", start),
		zap.Int("end", end),
	)
	auditor.Logger.Debug("Set pageLimit",
		zap.Int("pageLimit", pageLimit),
	)

	auditor.Logger.Debug("Exited computeSliceBounds successfully")

	return start, end, pageLimit, nil
}

// parseSocial parses the social data for a user.
func (s *User) parseSocial() error {
	if string(s.SocialRaw[:]) == "[]" {
		// Fuck Pixiv
		return nil
	}

	err := json.Unmarshal(s.SocialRaw, &s.Social)
	if err != nil {
		return err
	}
	return nil
}

// IntStringMap is a custom type that can handle both JSON objects and empty arrays.
type IntStringMap map[int]string

// UnmarshalJSON implements custom unmarshalling for IntStringMap.
func (m *IntStringMap) UnmarshalJSON(data []byte) error {
	// Check if the data is an empty array.
	if string(data) == "[]" {
		*m = make(map[int]string)
		return nil
	}

	// Otherwise, expect an object.
	var temp map[string]*string
	if err := json.Unmarshal(data, &temp); err != nil {
		return err
	}

	result := make(map[int]string)
	for keyStr, value := range temp {
		key, err := strconv.Atoi(keyStr)
		if err != nil {
			return fmt.Errorf("invalid key '%s': %v", keyStr, err)
		}
		if value != nil {
			result[key] = *value
		} else {
			result[key] = ""
		}
	}

	*m = result
	return nil
}

func numberGreaterThan(l, r string) bool {
	if len(l) > len(r) {
		return true
	}
	if len(l) < len(r) {
		return false
	}
	return l > r
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}
